﻿using System;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Diagnostics;
using System.Web;
using System.Web.SessionState;
using log4net.Config;
using Mirabeau.Web.Session.UnitTests.Helpers;
using NUnit.Framework;

namespace Mirabeau.Web.Session.UnitTests
{
    /// <summary>
    /// Tests for <see cref="MemcachedSessionStateStoreProvider"/>
    /// </summary>
    [TestFixture]
    public class ProviderTests
    {
        private const string memcachedExecutable = "Server\\memcached.exe";
        private Process memcachedServerProcess;
        private SessionStateStoreProviderBase memcachedProvider;

        /// <summary>
        /// Setup once before all tests.
        /// </summary>
        [TestFixtureSetUp]
        public void FixtureSetup()
        {
            XmlConfigurator.Configure();
            StartMemcachedInstance();

            memcachedProvider = new MemcachedSessionStateStoreProvider();
            NameValueCollection config = new NameValueCollection();
            config["applicationname"] = "dashboard";
            config["lockTimeout"] = 30.ToString();
            memcachedProvider.Initialize("MemcachedSessionStateStoreProvider", config);
        }

        private void StartMemcachedInstance()
        {
            memcachedServerProcess = Process.Start(memcachedExecutable);
        }

        /// <summary>
        /// Clean up once after all tests.
        /// </summary>
        [TestFixtureTearDown]
        public void FixtureTearDown()
        {
            if (memcachedProvider != null)
            {
                memcachedProvider.Dispose();
            }

            StopMemcachedInstance();
        }

        private void StopMemcachedInstance()
        {
            if (memcachedServerProcess != null)
            {
                memcachedServerProcess.Kill();
            }
        }

        /// <summary>
        /// GetItemExclusive must lock session item data for duration of request.
        /// </summary>
        [Test]
        public void GetItemExclusiveMustLockSessionItemDataForDurationOfRequest()
        {
            WithHttpRequest((context, provider) =>
            {
                SessionStateActions actions;
                object lockId;
                TimeSpan lockAge;
                bool isLocked;
                string sessionId = context.Session.SessionID;
                                    
                // act: GetItemExclusive is called when session state is enabled
                SessionStateStoreData retreivedStore = provider.GetItemExclusive(context, sessionId, out isLocked, out lockAge, out lockId, out actions);

                Assert.That(retreivedStore.Items, Is.Not.Null);
                Assert.That(isLocked, Is.True);
                Assert.That(lockId, Is.Not.Null);
                Assert.That(lockId, Is.Not.EqualTo(Guid.Empty));
            });
        }


        /// <summary>
        /// Releases the item exclusivelock on item.
        /// </summary>
        [Test]
        public void ReleaseItemExclusiveReleasesLockOnItem()
        {
            WithHttpRequest((context, provider) =>
            {
                SessionStateActions actions;
                object lockId;
                TimeSpan lockAge;
                bool isLocked;
                string sessionId = context.Session.SessionID;
                provider.GetItemExclusive(context, sessionId, out isLocked, out lockAge, out lockId, out actions);

                //act
                provider.ReleaseItemExclusive(context, sessionId, lockId);

                provider.GetItem(context, sessionId, out isLocked, out lockAge, out lockId, out actions);
                Assert.That(isLocked, Is.False);
                Assert.That(lockId, Is.EqualTo(Guid.Empty));
                Assert.That(actions, Is.EqualTo(SessionStateActions.None));
            });
        }

        /// <summary>
        /// GetItemExclusive must return null when no data is found.
        /// </summary>
        [Test]
        public void GetItemExclusiveMustReturnNullWhenNoDataIsFound()
        {
            //arrange
            WithHttpRequest((context, provider) =>
            {
                SessionStateActions actions;
                object lockId;
                TimeSpan lockAge;
                bool isLocked;
                const string sessionId = "notexistingId";

                //act
                SessionStateStoreData retreivedStore = provider.GetItemExclusive(context, sessionId, out isLocked, out lockAge, out lockId, out actions);

                Assert.That(retreivedStore, Is.Null);
            });
        }

        /// <summary>
        /// GetItemExclusive Must Return Locked is True And Set LockAge To Current Time Minus Lock Time
        /// </summary>
        [Test]
        public void GetItemExclusiveMustReturnLockedAsTrueAndSetLockAgeToCurrentTimeMinusLockTime()
        {
            WithHttpRequest((context, provider) =>
            {
                SessionStateActions actions;
                object lockId;
                TimeSpan lockAge;
                bool isLocked;
                string sessionId = context.Session.SessionID;
                
                // act
                provider.GetItemExclusive(context, sessionId, out isLocked, out lockAge, out lockId, out actions);

                Assert.That(isLocked, Is.True);
                Assert.That(lockAge.Ticks, Is.GreaterThan(0));
            });
        }

        /// <summary>
        /// SetAndReleaseItemExclusive must update item with supplied values and releases lock.
        /// </summary>
        [Test]
        public void SetAndReleaseItemExclusiveMustUpdateItemWithSuppliedValuesAndReleasesLock()
        {
            WithHttpRequest((context, provider) =>
            {
                SessionStateActions actions;
                object lockId;
                TimeSpan lockAge;
                bool isLocked;
                string sessionId = context.Session.SessionID;
                SessionStateStoreData retreivedStore = provider.GetItemExclusive(context, sessionId, out isLocked, out lockAge, out lockId, out actions);
                retreivedStore.Items["AddData"] = "Value1";

                // act
                provider.SetAndReleaseItemExclusive(context, sessionId, retreivedStore, lockId, false);
                
                retreivedStore = provider.GetItem(context, sessionId, out isLocked, out lockAge, out lockId, out actions);
                Assert.That(isLocked, Is.False);
                Assert.That(lockAge.Ticks, Is.EqualTo(0));
                Assert.That(lockId, Is.EqualTo(Guid.Empty));
                Assert.That(retreivedStore.Items["AddData"], Is.EqualTo("Value1"));
            });
        }

        /// <summary>
        /// SetAndReleaseItemExclusive must Create a new item with supplied values.
        /// </summary>
        [Test]
        public void SetAndReleaseItemExclusiveMustCreateNewItemWithSuppliedValuesAndReleasesLock()
        {
            WithHttpRequestNoCreateUnitializedItem((context, provider) =>
            {
                SessionStateActions actions;
                object lockId = Guid.Empty;
                TimeSpan lockAge;
                bool isLocked;
                string sessionId = context.Session.SessionID;
                SessionStateStoreData retreivedStore = provider.CreateNewStoreData(context, 60);
                retreivedStore.Items["AddData"] = "Value1";

                // act
                provider.SetAndReleaseItemExclusive(context, sessionId, retreivedStore, lockId, true);

                retreivedStore = provider.GetItem(context, sessionId, out isLocked, out lockAge, out lockId, out actions);
                Assert.That(isLocked, Is.False);
                Assert.That(lockAge.Ticks, Is.EqualTo(0));
                Assert.That(lockId, Is.EqualTo(Guid.Empty));
                Assert.That(retreivedStore.Items["AddData"], Is.EqualTo("Value1"));
            });
        }

        /// <summary>
        /// RemoveITem must remove item with given parameter
        /// </summary>
        [Test]
        public void RemoveItemMustRemoveItemWithGivenParameter()
        {
            WithHttpRequest((context, provider) =>
            {
                SessionStateActions actions;
                object lockId;
                TimeSpan lockAge;
                bool isLocked;
                string sessionId = context.Session.SessionID;
                SessionStateStoreData sessionStateStoreData = provider.GetItem(context, sessionId, out isLocked, out lockAge, out lockId, out actions);

                //act
                provider.RemoveItem(context, sessionId, lockId, sessionStateStoreData);

                sessionStateStoreData = provider.GetItemExclusive(context, sessionId, out isLocked, out lockAge, out lockId, out actions);
                Assert.That(sessionStateStoreData, Is.Null);
            });
        }

        /// <summary>
        /// Test if the method does not give errors.
        /// </summary>
        [Test]
        public void ResetItemTimeoutMustUpdateTimeout()
        {
            //arrange
            WithHttpRequest((context, provider) =>
            {
                string sessionId = context.Session.SessionID;

                //act
                provider.ResetItemTimeout(context, sessionId);
            });
        }

        /// <summary>
        /// An exception must be thrown when a item is being added as new, and there is already an item present with that id.
        /// </summary>
        [Test]
        public void SetAndReleaseItemMustThrowProviderExceptionWhenNewItemIsAddedAndItAlreadyExists()
        {
            WithHttpRequestNoCreateUnitializedItem((context, provider) =>
            {
                object lockId = Guid.Empty;
                string sessionId = context.Session.SessionID;
                SessionStateStoreData retreivedStore = provider.CreateNewStoreData(context, 60);
                retreivedStore.Items["AddData"] = "Value1";

                // act
                provider.SetAndReleaseItemExclusive(context, sessionId, retreivedStore, lockId, true);

                // act & assert
                Assert.That(
                    () => provider.SetAndReleaseItemExclusive(context, sessionId, retreivedStore, lockId, true)
                    , Throws.TypeOf(typeof(ProviderException)));
            });
        }

        /// <summary>
        /// Creates a simulated http context an initializes the memcachedProvider with the session of the simulated context.
        /// </summary>
        /// <param name="actionToExecute">The action to execute.</param>
        public void WithHttpRequest(Action<HttpContext, SessionStateStoreProviderBase> actionToExecute)
        {
            using (new HttpSimulator().SimulateRequest())
            {
                HttpContext context = HttpContext.Current;
                memcachedProvider.InitializeRequest(context);
                memcachedProvider.CreateUninitializedItem(context, context.Session.SessionID, 60);
                actionToExecute.Invoke(context, memcachedProvider);
            }
        }

        /// <summary>
        /// Creates a simulated http context an initializes the memcachedProvider with the session of the simulated context.
        /// </summary>
        /// <param name="actionToExecute">The action to execute.</param>
        public void WithHttpRequestNoCreateUnitializedItem(Action<HttpContext, SessionStateStoreProviderBase> actionToExecute)
        {
            using (new HttpSimulator().SimulateRequest())
            {
                HttpContext context = HttpContext.Current;
                memcachedProvider.InitializeRequest(context);
                actionToExecute.Invoke(context, memcachedProvider);
            }
        }
    }
}